package oop.ex3.nameserver;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import oop.ex3.sharedclasses.IPaddress;

/**
 * Represents a NameServer.
 * @author zablo
 *
 */
public class NameServer {
	
	/*
	 * The map key set contains known fileManager's IPaddress.
	 * it fits them to a TreeSet of fileManager's file names
	 */
	private ConcurrentHashMap<IPaddress, TreeSet<String>> _fileManagerFiles;
	
	// LinkedList of fileManagers' NameServers (identified by IP+Port- IPaddress)
	private LinkedList<IPaddress> _fileManagerServers;

	private ServerSocket _serverSocket;
	private int _port;
	
	private static final int TIMEOUT_5_SECONDS = 5000;
	
	/**
	 * Constructs a new NameServer.
	 * Initiates the Server's data-storing data structures.
	 */
	public NameServer(int port){
		// this._fileManagers = new LinkedList<IPaddress>();
		this._fileManagerFiles = new ConcurrentHashMap<IPaddress, TreeSet<String>>();
		this._fileManagerServers = new LinkedList<IPaddress>();
		this._port=port;
		try {
			this._serverSocket = new ServerSocket(port);
			this._serverSocket.setSoTimeout(TIMEOUT_5_SECONDS);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Checks whether a client with the given IPaddress is known to the NS.
	 * Returns true if the FM with the given IPaddress is known to this nameServer,
	 * false otherwise.
	 * @param ipAddress the IPaddress (IP+port) to be checked.
	 * @return true if known the the NS, false otherwise.
	 */
	public boolean knowsClient(IPaddress ipAddress) {
		//for (IPaddress knownFM: this._fileManagers)
		synchronized (_fileManagerFiles) {
		for (IPaddress knownFM: this._fileManagerFiles.keySet()){
			if (knownFM.equals(ipAddress)){
				return true;
			}
		}
		}
		return false;
	}
	
	/**
	 * Adds a new client to the server's Database.
	 * will add the set of IP and TreeSet of files of the client to the _fileManagerFiles list
	 * and will add all the client's known NSs to the _fileManagerServers list.
	 * @param ipAddress the new client's IPaddress.
	 * @param fileManagerFiles the new client's files TreeSet<String>
	 * @param fileManagerServers the new client's NameServers list.
	 */
	public void addNewClient(IPaddress ipAddress, TreeSet<String> fileManagerFiles, 
							LinkedList<IPaddress> fileManagerServers){
		//add the client's IP to the NS known FMs
		// synchronized(this._fileManagers){
		// this._fileManagers.add(ipAddress);
		// }
		
		//add the client's IP & Files to the NS known files
		synchronized(this._fileManagerFiles){
			this._fileManagerFiles.put(ipAddress, fileManagerFiles);
		}
		// add the client's NameServers to the NS known NameServers
		synchronized(this._fileManagerServers){
			for (IPaddress nameServerAddress: fileManagerServers){
				if (!knowsNameServer(nameServerAddress)){
					this._fileManagerServers.add(nameServerAddress);
				}
			}
		}
		
	}
	
	/**
	 * Checks if this Nameserver knows another nameServer.
	 * @param nameServer the name server to be checked
	 * @return true if indeed knows it, false otherwise.
	 */
	private boolean knowsNameServer(IPaddress nameServer){
		synchronized(_fileManagerServers){
			for (IPaddress server: _fileManagerServers){
				if (server.equals(nameServer)){
					return true;
				}
			}
			return false;
		}
	}
	
	/**
	 * Removes a client from the server's Database.
	 * will remove the IPaddress & files TreeSet from the database.
	 * @param sessionIP the to-be-removed client's IPaddress.
	 */
	public void removeClient(IPaddress sessionIP) {
		synchronized (_fileManagerFiles) {
			for (IPaddress knownClient: _fileManagerFiles.keySet())
				if(knownClient.equals(sessionIP)){
					_fileManagerFiles.remove(knownClient);
					break;
				}
		}
	}

	/**
	 * Returns the server's ServerSocket.
	 * @return the server's ServerSocket.
	 */
	public ServerSocket getServerSocket(){
		return this._serverSocket;
	}
	
	/**
	 * Restarts the server's ServerSocket.
	 * Will be used in case of timeout for example.
	 */
	public void restartServerSocket(){
		try {
			this._serverSocket = new ServerSocket(this._port);
			this._serverSocket.setSoTimeout(TIMEOUT_5_SECONDS);
		} catch (IOException e) {
			// TODO Auto-generated catch block
		}
	}
	
	/** 
	 * Sets the server to be Offline - closes its ServerSocket.
	 * therefore, no new connections can be accepted.
	 */
	public void setOffline(){
		try {
			this._serverSocket.close();
		} catch (IOException e) {
			//TODO check exception
			// Assuming this exception can be ignored
		}
	}
	
	/**
	 * Returns all the files known to this server.
	 * Returns a TreeSet of file-name Strings, alphabetically ordered,
	 * containing all the files known to this server.
	 * @return a TreeSet<String> containing all the files known to the server.
	 */
	public TreeSet<String> getAllKnownFiles(){
		TreeSet<String> allKnownFiles=new TreeSet<String>();
		synchronized (this._fileManagerFiles) {
			// runs on all known files
			for (TreeSet<String> filesTree: this._fileManagerFiles.values()){
				for (String file: filesTree){
					allKnownFiles.add(file);
				}
			}
		}
		return allKnownFiles;
	}
	
	/**
	 * Returns all the NameServers known to this server.
	 * Returns a LinkedList of IPaddresses (IP & port object), which contains
	 * all the NameServers known to this server, in the order 
	 * they were received by it.
	 * @return a LinkedList<IPaddress> containing all the NameServers known to
	 * this server.
	 */
	public LinkedList<IPaddress> getAllKnownNameServers(){
		LinkedList<IPaddress> allKnownNameServers=new LinkedList<IPaddress>();
		synchronized (_fileManagerServers) {
			//Creating a deep copy to avoid concurrency problems
			for (IPaddress nameServerAddress: this._fileManagerServers){
				allKnownNameServers.add(new IPaddress(nameServerAddress));
			}
		}
		return allKnownNameServers;
	}
	
	/**
	 * Returns all the FileManagers containing file wantedFile, 
	 * which are known to the server. 
	 * @param wantedFile the file-name to be searched for.
	 * @return a LinkedList<IPaddress> of FileManager (IP+port) addresses, 
	 *     when	each of them is known to this server and 
	 *     contains file <wantedFile>.
	 */
	public LinkedList<IPaddress> getFileSources(String wantedFile){
		LinkedList<IPaddress> fileSources=new LinkedList<IPaddress>();
		synchronized (_fileManagerFiles) {
			for (IPaddress fileSource: _fileManagerFiles.keySet()){
				if (_fileManagerFiles.get(fileSource).contains(wantedFile)){
					fileSources.add(new IPaddress(fileSource));
				}
			}
		}
		return fileSources;
	}
	
	/**
	 * Adds a new file to the NameServer.
	 * adds fileName as a file which is originally from clientIPAddress
	 * to the _fileManagerFiles hashMap.
	 * @param clientIPAddress the Client's IPaddress.
	 * @param fileName the Client's fileName (that will be added).
	 */
	public void addFile(IPaddress clientIPAddress, String fileName){
		synchronized (_fileManagerFiles) {
			for (IPaddress knownIP: _fileManagerFiles.keySet()){
				if (knownIP.equals(clientIPAddress)){
					_fileManagerFiles.get(knownIP).add(fileName);
					break;
				}
			}
		}
	}
	
	/**
	 * Removes a file from the NameServer.
	 * removes fileName from clientIPAddress's file tree
	 * (in the _fileManagerFiles hashMap).
	 * @param clientIPAddress the Client's IPaddress.
	 * @param fileName the Client's fileName (that will be removed).
	 */
	public void removeFile(IPaddress clientIPAddress, String fileName) {
		synchronized(_fileManagerFiles){
			for (IPaddress knownIP: _fileManagerFiles.keySet()){
				if (knownIP.equals(clientIPAddress)){
					_fileManagerFiles.get(knownIP).remove(fileName);
					break;
				}
			}
		}
	}
	
	
	//TODO REMOVE DEBUGGING METHOD
	public void printDB(){
		System.out.println("@@@    @@@    @@@");
		System.out.println("Printing Database");
		System.out.println("Known FileManagers:");
		for (IPaddress a:_fileManagerFiles.keySet()){
			System.out.println("  "+a.getIP()+"%"+a.getPort());
		}
		System.out.println();
		System.out.println("Known Files:");
		for (IPaddress a: _fileManagerFiles.keySet()){
			System.out.println("  Files at: "+a.getIP()+"%"+a.getPort());
			for (String s: _fileManagerFiles.get(a)){
				System.out.println("      "+s);
			}
		}
		System.out.println();
		System.out.println("Known NameServers:");
		for (IPaddress a: _fileManagerServers){
			System.out.println("  "+a.getIP()+"%"+a.getPort());
		}
		System.out.println(" End Of Database");
		System.out.println("@@@    @@@    @@@");
		
	}


	
}
